import pickle
import threading
import os
import time
import types
import sys
import traceback
import inspect

from activist.DataStore import DataStore
from activist.Data import Data
import activist.MetaData
from activist.Config import Config
import activist.Time
from activist import errors

from twisted.internet import defer
from twisted.internet import reactor

class Proxy:
    def __init__(self,data):
        self.__id = data.id()
        self.__class = data.__class__
    def unproxy(self):
        if not DataStore.has_key(self.__id):
            raise errors.UnproxyError("NO SUCH DATA:"+str(self.__id))
        if DataStore[self.__id].__class__ != self.__class:
            raise errors.UnproxyError("Proxy expected " + str(self.__class) + " not " + str(DataStore[self.__id].__class__))
        return DataStore[self.__id]
    def __str__(self):
        return '<PROXY for %s>' % (self.__id,)
    def __repr__(self):
        return '<PROXY for %s>' % (self.__id,)


def performCheckpoint(callagain=True):
    import time, os.path
    
    filename = os.path.join(Config.base,time.strftime('checkpoint--%B-%d-%Y--%s.xml'))
    try:
        file = open(filename,'w')
    except IOError:
        sendMessageToAllAdministrators("Checkpoint failed; couldn't open \"%s\"." % filename)
    else:
        DataStore.checkpointSingleXML(file)
        file.close()
        from activist.util import sendMessageToAllAdministrators
        #sendMessageToAllAdministrators("Performed checkpoint; %d objects (data and metadata)." % (len(DataStore._objs),))

    CommandProcessor.assertConsistency()

    if callagain:
        reactor.callLater(Config.seconds_per_checkpoint, performCheckpoint)


class _CommandProcessor:

    def __init__(self):
        self.logdir = os.path.join(Config.base,'data/commands.log')
        self.count = 0
        self.__loading = False
        self.next_id = 1
        self.next_meta_id = 1
        self.consistent = []
        self.CONSISTENT = "consistent"
        
        for x in os.listdir(self.logdir):
            try:
                if int(x) >= self.count:
                    self.count = int(x) + 1
            except ValueError:
                pass
        #self.loadAndProcess()



    def generateNewId(self):
        rtn = str(self.next_id)
        self.next_id = self.next_id + 1
        return rtn
    
    def generateNewMetaId(self):
        rtn = 'meta-' + str(self.next_meta_id)
        self.next_meta_id = self.next_meta_id + 1
        return rtn

    def restartToSpecificConsistencyPoint(self,consist):
        if os.path.isfile(os.path.join(self.logdir,'rebooting')):
            raise "Cannot reboot; already flagged to do so."
        
        if consist in self.consistent:
            rebooting = open( os.path.join(self.logdir,'rebooting'), 'w' )
            rebooting.write(str(consist))
            rebooting.close()

                                        # for fun and safety, do a checkpoint
            performCheckpoint(callagain=False)

            def reboot():
                reactor.stop()
            reactor.callLater(10,reboot)
            

    def assertConsistency(self):
        """
        this is a crude "transaction" thing; the administrator may
        rollback to any of these points, which are asserted throughout
        the code (e.g. after creating an event, uploading media, etc.)
        """
        self.serializeCommand(self.CONSISTENT,0,None,(),{})
        self.consistent.append(self.count)


    def serializeCommand(self,commandname,version,object,*args,**kwargs):
        if self.__loading:
            return

                                        # this is for debugging; often
                                        # we want the "real" stack
                                        # from when the call was made,
                                        # not into CommandProcessor as
                                        # the commands are being
                                        # deserialized; this saves the
                                        # original trace is case there
                                        # is a problem later restoring
                                        # it. non-ideal, perhaps, but
                                        # the best I can come up with.
        stack = inspect.stack()
        try:
            trace = ''
            for x in stack:
                if x[4] == None:
                    break
                desc = x[4][0]
                trace = trace + "%s, line %d:\n%s" % (x[1],x[2],desc)
        finally:
            for x in stack:
                del x
            del stack
        
        #print "serializeCommand:",commandname,version,object,args,kwargs
        #print "writing command:",'%010d' % self.count

                                        # prepent type and desire ID
                                        # if we're creating something

        if commandname == 'create':
            args = (object.__class__,object._hash) + args
        
        log = open( os.path.join(self.logdir,'%010d' % self.count), 'w' )
        try:
            stuff = self.recursiveProxyArgs((commandname,version,object,args,kwargs))
        except:
            print "ERROR proxying things",(commandname,version,object,args,kwargs)
            raise
        pickle.dump(trace,log)
        pickle.dump(stuff,log)
        log.close()
        self.count = self.count + 1

    def recursiveProxyArgs(self,args):
        if isinstance(args,list) or isinstance(args,tuple):
            rtn = []
            for arg in args:
                if isinstance(arg,tuple) or isinstance(arg,list) or isinstance(arg,dict):
                    rtn.append(self.recursiveProxyArgs(arg))
                elif isinstance(arg,int) or isinstance(arg,float) or isinstance(arg,basestring):
                    rtn.append(arg)
                elif isinstance(arg,defer.Deferred):
                    if not hasattr(arg,"result"):
                        raise "Deferred doesn't have result yet!"
                    else:
                        rtn.append(Proxy(arg.result))
                elif isinstance(arg,Data):
                    rtn.append(Proxy(arg))
                elif arg.__class__ in [activist.MetaData.TypeStringFudger.FudgerType,types.ModuleType,types.ClassType,types.NoneType,types.TypeType] or isinstance(arg,activist.Time.Time):
                    rtn.append(arg)
                else:
                    raise str(("Cannot serialize overly-complex type:",arg.__class__))
            if isinstance(args,tuple):
                rtn = tuple(rtn)
        elif isinstance(args,dict):
            rtn = {}
            for (key,arg) in args.items():
                if isinstance(arg,tuple) or isinstance(arg,list) or isinstance(arg,dict):
                    rtn[key] = self.recursiveProxyArgs(arg)
                elif isinstance(arg,int) or isinstance(arg,float) or isinstance(arg,str):
                    rtn[key] = arg
                elif isinstance(arg,defer.Deferred):
                    if not hasattr(arg,"result"):
                        raise "Deferred doesn't have result yet!"
                    else:
                        rtn[key] = Proxy(arg.result)
                elif isinstance(arg,Data):
                    rtn[key] = Proxy(arg)
                elif arg.__class__ in [types.ModuleType,types.ClassType,types.NoneType,types.TypeType] or isinstance(arg,activist.Time.Moment):
                    rtn[key] = arg
                else:
                    raise "Cannot serialize overly-complex type:",arg
        return rtn

    def unproxy(self,args):
        if isinstance(args,list) or isinstance(args,tuple):
            rtn = []
            for arg in args:
                if isinstance(arg,Proxy):
                    rtn.append(arg.unproxy())
                else:
                    rtn.append(self.unproxy(arg))
            if isinstance(args,tuple):
                rtn = tuple(rtn)
        elif isinstance(args,dict):
            rtn = {}
            for (key,arg) in args.items():
                if isinstance(arg,Proxy):
                    rtn[key] = arg.unproxy()
                else:
                    rtn[key] = self.unproxy(arg)
        else:
            return args
        return rtn
        


    def create(self,klass,expectedid,version,*args,**kwargs):
        if self.__loading:
            x = int(expectedid) + 1
            if x > self.next_id:
                self.next_id = x
                
        object = klass(expectedid)
        #print "CREATING",klass,expectedid#,args,kwargs
        object._command_execute("create",version,*args,**kwargs)
        if object.id() != expectedid:
            print klass,expectedid,version,args,kwargs
            raise "Expected id \"%s\" but got \"%s\" instead! (for class \"%s\"" % (expectedid,object.id(),str(klass))
        else:
            if Config.debug: print "CHECK:",klass,expectedid
        return object

    def loadAndProcess(self,step=False,wait=True):
        print "Loading and processing commands..."
        self.__loading = True

        rebootTo = None
        rebootDestroyed = None
        if os.path.isfile(os.path.join(self.logdir,'rebooting')):
            print "  REBOOT flag found; grokking..."
            try:
                rebootTo = int(open(os.path.join(self.logdir,'rebooting'),'r').read())
                os.unlink(os.path.join(self.logdir,'rebooting'))
            except:
                rebootTo = None

            print "  rebooting to command:",rebootTo

            
        if step:
            skip = None
            olddebug = Config.debug
            Config.debug = True
        try:
            self.count = 0
            commands = os.listdir(self.logdir)
            commands.sort()

                                        # the "next_id" is incremented
                                        # in self.create only-if we're
                                        # loading (and will be biggest
                                        # ID reached + 1 after this is
                                        # all done). meta-ids always
                                        # start at 1 and are only used
                                        # by MetaData, so only created
                                        # via commands (which are all
                                        # serialized)
            self.next_id = 1
            self.next_meta_id = 1
            stop = False
            
            for commandfile in commands:
                if stop:
                    from activist.util import sendMessageToAllAdministrators
                    sendMessageToAllAdministrators("While starting up, there was an exception trying to process command %d. This is probably bad and should be fixed by either rolling-back the database or filing a problem-report with the developers." % (self.count,))
                    break
                if commandfile == '.svn':
                    continue

                if step:
                    if not skip or int(commandfile) > skip:
                        skip = None
                        print "next_id=",self.next_id," next_meta_id=",self.next_meta_id
                        if wait:
                            print "Press RTN to execute command",commandfile
                            foo = raw_input()
                            try:
                                skip = int(foo)
                            except:
                                pass

                if rebootTo and self.count >= rebootTo:
                    self.assertConsistency()
                    rebootDestroyed = 0
                    for f in commands:
                        try:
                            x = int(f)
                        except ValueError:
                            continue
                        if x > rebootTo:
                            rebootDestroyed = rebootDestroyed + 1
                            file = os.path.join(self.logdir,f)
                            print "deleting:",file
                            os.unlink(file)
                    
                    print "next meta-id,id:",self.next_meta_id,self.next_id
                    self.count = rebootTo+1
                    break
                    

                try:
                    file = open(os.path.join(self.logdir,commandfile),'r')
                    trace = pickle.load(file)
                    cmd = pickle.load(file)
                    file.close()
                except:
                    print "FAILED to load command:",commandfile
                    # trace = traceback.extract_tb(sys.exc_traceback)
                    # raise the traceback from the original call to the command's function
                    print trace
                    print cmd
                    print "next meta-id:",self.next_meta_id
                    raise

                self.count = int(commandfile) + 1
                (commandname,version,object,args,kwargs) = cmd
                if Config.debug: print "   ",version,commandname,object,str(args)[:30]
                try:
                    args = self.unproxy(args)
                except errors.UnproxyError:
                    print "FAILED to unproxy args for command",commandfile
                    print trace
                    print cmd
                    print "next meta-id:",self.next_meta_id
                    raise
                    stop = True
                    continue
                try:
                    kwargs = self.unproxy(kwargs)
                except errors.UnproxyError:
                    if 1:#step:
                        print "FAILED to unproxy kwargs",commandfile
                        print trace
                        print cmd
                        print "next meta-id:",self.next_meta_id
                    raise
                    stop = True
                    continue

                if commandname == self.CONSISTENT:
                    self.consistent.append(int(commandfile))
                    print "CONSISTENT at:",self.consistent[-1]
                    continue
                
                if commandname == "create":
                    klass = args[0]
                    expectedid = args[1]
                    if step: print "creating",klass,args[1:],kwargs
                    object = self.create(klass,expectedid,version,*tuple(args[2:]),**kwargs)
                    if step: print "   created:",object
                    continue

                if step: print cmd
                    
                if object and not isinstance(object,str):
                    try:
                        object = object.unproxy()
                    except errors.UnproxyError:
                        if 1:#step:
                            print "FAILED to unproxy args",commandfile
                            print trace
                            print cmd
                            print "next meta-id:",self.next_meta_id
                        raise
                        stop = True
                        continue

                object._command_execute(commandname,version,*args,**kwargs)

        finally:
            self.__loading = False
            if step:
                Config.debug = olddebug

        if self.count == 0:
            print "no commands processed; presuming raw start and initializing"
            from activist.Config import bootstrapInitialize
            bootstrapInitialize()
            self.assertConsistency()

        else:
            #from activist.util import sendMessageToAllAdministrators
            #sendMessageToAllAdministrators("Activist.ca restored. %d items created (current ID at %d)." % (self.count,self.next_id))
            ## do a checkpoint, which auto-schedules the next one
            performCheckpoint()
            

        if rebootTo and rebootDestroyed is not None:
            from activist.util import sendMessageToAllAdministrators
            sendMessageToAllAdministrators("I was told to boot up and stop at command #%d; I have reached that command and am stopping. I have destroyed all the commands after #%d (there were %d of them)."%(rebootTo,rebootTo,rebootDestroyed))
                    


CommandProcessor = _CommandProcessor()

